CSRF攻击必须依次完成以下两个条件:
- 登录受信任网站A,并在本地生成Cookie。
- 在不登出A的情况下,访问危险网站B。
关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了
CSRF攻击形成的原因:
- 网站违反了HTTP协议,使用GET请求更新资源(非常危险).
- 使用无校验的表单
CSRF攻击是源于WEB的隐式身份验证机制!(比如你在A网站登录后,在同一个浏览器的tab页打开B网站网页, 而B网站网页有一些脚本链接到A网站并偷偷的发送请求更新你账号的信息.如果没有防CSRF攻击,这样是可以实现的.)
WEB的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的!
CSRF防范方式:客户端页面增加伪随机数。每次需要修改该用户的数据库信息时必须带上该随机数,否则不受理.
解决办法: 在Form表单加一个hidden field,里面是服务端生成的足够随机数的一个Token(恶意网站猜不到也无法获取到相同的Token), 然后使用一个拦截器interceptor来检查每一个非get请求, 看该token与服务器token是否一致,不一致的不受理该请求.
以下是token 的工具管理类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| package com.xxx.xxx.util; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.util.UUID; /** * CSRFtoken管理类 * * @author 作者 yss * @version 版本号 v1.0 */ public final class CSRFTokenManager { /** * 约定规范:表单提交时的token的input的name属性必须为该值才能获取到后台返回的token。 * 下面是使用EL表达式接收从后台返回的token参数 * 如: <input type="hidden" id="CSRFToken" value="${CSRFToken}"> */ public static final String CSRF_PARAM_NAME = "CSRFToken"; /** * 存放在session中的token名称(跟上面的name属性值不一定一样) */ public static final String CSRF_TOKEN_FOR_SESSION_ATTR_NAME = CSRFTokenManager.class.getName() + ".tokenval"; /** *从session中获取token字符串 * @param session * @return */ public static String getTokenForSession(HttpSession session) { String token = null; //保证session只存在一个token,避免多线程情况下产生冲突。 synchronized (session) { token = (String) session.getAttribute(CSRF_TOKEN_FOR_SESSION_ATTR_NAME);//尝试获取session中的token if (null == token) {//如果session中没有token,就重新生成一个token token = UUID.randomUUID().toString(); session.setAttribute(CSRF_TOKEN_FOR_SESSION_ATTR_NAME, token); } } return token; } /** *获取到request中的token值。 * @param request * @return */ public static String getTokenFromRequest(HttpServletRequest request) { return request.getParameter(CSRF_PARAM_NAME); } //构造器 private CSRFTokenManager() { } }
|
后台返回token参数给页面:
String token = CSRFTokenManager.getTokenForSession(request.getSession());//uuid生成的随机token
modelAndView.addObject(CSRFTokenManager.CSRF_PARAM_NAME, token);//添加token参数 <%–token–%>
编写代码要符合HTTP规范,不要使用GET请求更新资源
在非GET请求中带上token 参数(ajax请求也要),然后使用拦截器检查.
对于受到CSRF攻击使用的是抛自定义异常,然后使用springmvc全局异常进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| package com.xxx.xxx.interceptor; import com.xxx.xxx.exception.CSRFException; import com.xxx.xxx.util.CSRFTokenManager; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 对于未登录用户的请求进行拦截,确保用户已经登录,然后进行后续的网页请求 * * @Author yss * @Version 1.0 * @see */ public class CSRFInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // Enumeration parasm = request.getParameterNames(); if (!"GET".equals(request.getMethod())) {//非get请求 String CSRFToken = CSRFTokenManager.getTokenFromRequest(request);//页面传过来的csrf参数 if (CSRFToken == null || !CSRFToken.equals(CSRFTokenManager.getTokenForSession(request.getSession()))) {//token不对应 throw new CSRFException("CSRF攻击");//抛异常后将会进入springmvc全局异常处理体系 } } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { super.postHandle(request, response, handler, modelAndView); } }
|
在spring的配置文件中添加拦截器使其生效
1 2 3 4 5 6 7
| <mvc:interceptors> <!-- 防止CSRF攻击的拦截器 --> <mvc:interceptor> <mvc:mapping path="/**"/> <bean id="CSRFInterceptor" class="com.xxx.xxx.interceptor.CSRFInterceptor"></bean> </mvc:interceptor> </mvc:interceptors>
|